day 5¶
このノートブックの実行例はこちら(HTML版)で確認できます
0. はじめに¶
ページ上部のメニューバーにある Kernel メニューをクリックし、プルダウンメニューから [Change Kernel ...] を選び、gssm2024:Python を選択してください。
ノートブック上部の右隅に表示されたカーネル名が gssm2024:Python になっていることを確認してください。
1. テキスト分析 (実践編)¶
1.0 事前準備¶
1.0.1 定義済み関数の読み込み¶
以下のセルを修正せずに実行してください
In [1]:
import warnings
warnings.simplefilter('ignore')
import gssm_utils
%matplotlib inline
1.0.1 データのダウンロード (前回ダウンロード済みのためスキップ)¶
以下のデータがダウンロード済みです
| ファイル名 | 件数 | データセット | 備考 |
|---|---|---|---|
| rakuten-1000-2023-2024.xlsx.zip | 10,000 | •レジャー+ビジネスの 10エリア •エリアごと 1,000件 (ランダムサンプリング) •期間: 2023/1~2024 GW明け |
本講義の全体を通して使用する |
In [2]:
# もし、再度ダウンロードが必要な場合は残りの行のコメントマーク「#」を除去して、このセルを再実行してください
# FILE_ID = "1EeCuDrfKdlsMxG9p3Ot7TIxfV9_f2smY"
# !gdown --id {FILE_ID}
# !unzip -o rakuten-1000-2023-2024.xlsx.zip
1.0.2 データの読み込み (DataFrame型)¶
In [3]:
import numpy as np
import pandas as pd
all_df = pd.read_excel("rakuten-1000-2023-2024.xlsx")
print(all_df.shape)
display(all_df.head())
(10000, 18)
| カテゴリー | エリア | 施設番号 | 施設名 | コメント | 総合 | サービス | 立地 | 部屋 | 設備・アメニティ | 風呂 | 食事 | 旅行の目的 | 同伴者 | 宿泊年月 | 投稿者 | 年代 | 性別 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | A_レジャー | 01_登別 | 80732 | 登別カルルス温泉 湯元オロフレ荘 | 彼のリクエストでカルルス温泉に宿泊させていただきました。北海道ラブ割利用でお得に宿泊できる分... | 5 | 5 | 4 | 5 | 5.0 | 5.0 | 5.0 | レジャー | 恋人 | 45231 | 投稿者 | na | na |
| 1 | A_レジャー | 01_登別 | 149334 | 天然温泉 幸鐘の湯 ドーミーイン東室蘭(ドーミーイン・御宿野乃 ホテルズグループ) | 室蘭方面の旅行の時には何度か宿泊しています。ドーミーインのサービスはとても良くて大好きです。... | 4 | 5 | 5 | 2 | 5.0 | 5.0 | 5.0 | レジャー | 家族 | 45413 | 投稿者 | na | na |
| 2 | A_レジャー | 01_登別 | 109022 | 心のリゾート 海の別邸ふる川 | もう一度行きたくなる宿でした。部屋もワンランクアップしていただき感謝しています。お風呂も最高... | 4 | 3 | 5 | 5 | 4.0 | 5.0 | 3.0 | レジャー | 一人 | 45261 | gaku0713 | 60代 | 男性 |
| 3 | A_レジャー | 01_登別 | 7506 | ホテルニューバジェット室蘭 | 居心地良く過ごせました | 4 | 4 | 4 | 4 | 4.0 | 4.0 | 3.0 | レジャー | 一人 | 45200 | 投稿者 | na | na |
| 4 | A_レジャー | 01_登別 | 37380 | 登別温泉 旅亭 花ゆら | 事前カード精算で露天風呂付特別和洋室を予約しましたが別の和室に案内されました。すぐ違うことに... | 1 | 2 | 4 | 2 | 2.0 | 4.0 | 3.0 | レジャー | 家族 | 45323 | 投稿者 | na | na |
1.0.3 「文書-抽出語」表 を作成する¶
コメント列から単語を抽出する (単語を品詞「名詞」「形容詞」「未知語」で絞り込む)
In [4]:
# 必要ライブラリのインポート
from collections import defaultdict
import MeCab
# mecab の初期化
tagger = MeCab.Tagger("-r ../tools/usr/local/etc/mecabrc --unk-feature 未知語")
# 単語頻度辞書の初期化
word_counts = defaultdict(lambda: 0)
# 抽出語情報リストの初期化
words = []
# 半角->全角変換マクロを定義する
ZEN = "".join(chr(0xff01 + i) for i in range(94))
HAN = "".join(chr(0x21 + i) for i in range(94))
HAN2ZEN = str.maketrans(HAN, ZEN)
# ストップワードを定義する
# stopwords = ['する', 'ある', 'ない', 'いう', 'もの', 'こと', 'よう', 'なる', 'ほう']
stopwords = ["湯畑"]
# データ1行ごとのループ
for index, row in all_df.iterrows():
# 半角->全角変換した後で, mecab で解析する
node = tagger.parseToNode(row["コメント"].translate(HAN2ZEN))
# 形態素ごとのループ
while node:
# 解析結果を要素ごとにバラす
features = node.feature.split(',')
# 品詞1 を取り出す
pos1 = features[0]
# 品詞2 を取り出す
pos2 = features[1] if len(features) > 1 else ""
# 原形 を取り出す
base = features[6] if len(features) > 6 else None
# 原型がストップワードに含まれない単語のみ抽出する
if base not in stopwords:
# 「名詞-一般」
if (pos1 == "名詞" and pos2 == "一般"):
base = base if base is not None else node.surface
postag = "名詞"
key = (base, postag)
# 単語頻度辞書をカウントアップする
word_counts[key] += 1
# 抽出語情報をリストに追加する
words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])
# 「形容動詞」
elif (pos1 == "名詞" and pos2 == "形容動詞語幹"):
base = base if base is not None else node.surface
base = f"{base}"
postag = "形容動詞"
key = (base, postag)
# 単語頻度辞書をカウントアップする
word_counts[key] += 1
# 抽出語情報をリストに追加する
words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])
# 「形容詞」
elif pos1 == "形容詞":
base = base if base is not None else node.surface
postag = "形容詞"
key = (base, postag)
# 単語頻度辞書をカウントアップする
word_counts[key] += 1
# 抽出語情報をリストに追加する
words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])
# 「未知語」
elif pos1 == "未知語":
base = base if base is not None else node.surface
postag = "未知語"
key = (base, postag)
# 単語頻度辞書をカウントアップする
word_counts[key] += 1
# 抽出語情報をリストに追加する
words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])
# 次の形態素へ
node = node.next
# DataFrme 型に整える
columns = [
"文書ID",
# "単語ID",
"表層",
"品詞",
"カテゴリー",
"エリア",
"dict_key",
]
docs_df = pd.DataFrame(words, columns=columns)
# DataFrame を表示する
print(docs_df.shape)
display(docs_df.head())
(158822, 6)
| 文書ID | 表層 | 品詞 | カテゴリー | エリア | dict_key | |
|---|---|---|---|---|---|---|
| 0 | 1 | リクエスト | 名詞 | A_レジャー | 01_登別 | (リクエスト, 名詞) |
| 1 | 1 | 温泉 | 名詞 | A_レジャー | 01_登別 | (温泉, 名詞) |
| 2 | 1 | ラブ | 名詞 | A_レジャー | 01_登別 | (ラブ, 名詞) |
| 3 | 1 | 割 | 名詞 | A_レジャー | 01_登別 | (割, 名詞) |
| 4 | 1 | 得 | 名詞 | A_レジャー | 01_登別 | (得, 名詞) |
抽出語の出現頻度をカウントする
In [5]:
# 「文書-抽出語」 表から単語の出現回数をカウントする
word_list = []
for i, (k, v) in enumerate(sorted(word_counts.items(), key=lambda x:x[1], reverse=True)):
word_list.append((i, k[0], v, k))
# DataFrame 型に整える
columns = [
"単語ID",
"表層",
"出現頻度",
"dict_key"
]
# DataFrame を表示する
word_counts_df = pd.DataFrame(word_list, columns=columns)
print(word_counts_df.shape)
display(word_counts_df.head(10))
(9060, 4)
| 単語ID | 表層 | 出現頻度 | dict_key | |
|---|---|---|---|---|
| 0 | 0 | 部屋 | 6768 | (部屋, 名詞) |
| 1 | 1 | 良い | 5397 | (良い, 形容詞) |
| 2 | 2 | ホテル | 3001 | (ホテル, 名詞) |
| 3 | 3 | 風呂 | 2692 | (風呂, 名詞) |
| 4 | 4 | ない | 2392 | (ない, 形容詞) |
| 5 | 5 | 美味しい | 2294 | (美味しい, 形容詞) |
| 6 | 6 | 温泉 | 1854 | (温泉, 名詞) |
| 7 | 7 | スタッフ | 1677 | (スタッフ, 名詞) |
| 8 | 8 | 立地 | 1532 | (立地, 名詞) |
| 9 | 9 | よい | 1517 | (よい, 形容詞) |
1.2 カテゴリーやエリアごとのユーザーの注目ポイントの評価の違いを見つける¶
1.2.1 「文書-抽出語」表の作成¶
「文書-抽出語」表を作成する (出現回数 Top 1000語)
In [6]:
# 「単語出現回数」 表から出現回数Top 1000語のみ抽出する
word_counts_1000_df = word_counts_df[0:1000]
# 「文書-抽出語」 表も出現回数Top 150語のみに絞り込む
merged_df = pd.merge(docs_df, word_counts_1000_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_1000_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]
# 「カテゴリー,エリア」でクロス集計する
cross_1000_df = pd.crosstab(
[
docs_1000_df['カテゴリー'],
docs_1000_df['エリア'],
docs_1000_df['文書ID']
],
docs_1000_df['単語ID'], margins=False
)
cross_1000_df.columns = word_counts_1000_df["表層"]
「文書-抽出語」表を {0,1} に変換する
In [7]:
# 「文書-抽出語」 表を {0,1} に変換する
cross_1000_df[cross_1000_df > 0] = 1
# DataFrame を表示する
print(cross_1000_df.shape)
display(cross_1000_df)
(9914, 1000)
| 表層 | 部屋 | 良い | ホテル | 風呂 | ない | 美味しい | 温泉 | スタッフ | 立地 | よい | ... | ほこり | 豊か | カラオケ | 支配人 | 頻繁 | 感覚 | 枕元 | コンサート | 店舗 | 人柄 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| カテゴリー | エリア | 文書ID | |||||||||||||||||||||
| A_レジャー | 01_登別 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 3 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 4 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 5 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| B_ビジネス | 10_福岡 | 9996 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 9997 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 9998 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 9999 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 10000 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
9914 rows × 1000 columns
1.2.2 共起行列を作成する (外部変数-抽出語)¶
In [8]:
# 「カテゴリー」のクロス集計と「エリア」のクロス集計を連結する
aggregate_df = pd.concat(
[
cross_1000_df.groupby(level='カテゴリー').sum(),
cross_1000_df.groupby(level='エリア').sum()
]
)
# DataFrame を表示する
print(aggregate_df.shape)
display(aggregate_df)
(12, 1000)
| 表層 | 部屋 | 良い | ホテル | 風呂 | ない | 美味しい | 温泉 | スタッフ | 立地 | よい | ... | ほこり | 豊か | カラオケ | 支配人 | 頻繁 | 感覚 | 枕元 | コンサート | 店舗 | 人柄 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A_レジャー | 2367 | 2134 | 789 | 1498 | 957 | 1486 | 1290 | 916 | 548 | 677 | ... | 4 | 16 | 16 | 13 | 11 | 12 | 5 | 1 | 1 | 17 |
| B_ビジネス | 2213 | 1705 | 1368 | 652 | 789 | 542 | 113 | 505 | 942 | 578 | ... | 11 | 4 | 3 | 3 | 8 | 9 | 15 | 18 | 19 | 3 |
| 01_登別 | 433 | 422 | 173 | 296 | 193 | 260 | 271 | 149 | 47 | 111 | ... | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2 |
| 02_草津 | 489 | 468 | 161 | 376 | 198 | 305 | 311 | 177 | 146 | 134 | ... | 1 | 3 | 4 | 3 | 8 | 1 | 0 | 0 | 0 | 5 |
| 03_箱根 | 544 | 454 | 158 | 331 | 199 | 373 | 234 | 222 | 69 | 156 | ... | 2 | 5 | 6 | 3 | 0 | 4 | 2 | 0 | 0 | 4 |
| 04_道後 | 408 | 380 | 228 | 190 | 157 | 177 | 231 | 130 | 184 | 135 | ... | 0 | 0 | 4 | 1 | 0 | 2 | 2 | 0 | 0 | 0 |
| 05_湯布院 | 493 | 410 | 69 | 305 | 210 | 371 | 243 | 238 | 102 | 141 | ... | 0 | 7 | 2 | 6 | 3 | 5 | 1 | 1 | 1 | 6 |
| 06_札幌 | 461 | 359 | 278 | 126 | 138 | 135 | 24 | 95 | 191 | 92 | ... | 1 | 1 | 0 | 1 | 3 | 4 | 2 | 4 | 3 | 1 |
| 07_名古屋 | 430 | 337 | 267 | 123 | 155 | 104 | 30 | 99 | 164 | 130 | ... | 1 | 0 | 1 | 0 | 1 | 0 | 4 | 2 | 5 | 0 |
| 08_東京 | 398 | 345 | 276 | 119 | 162 | 82 | 9 | 100 | 172 | 113 | ... | 3 | 0 | 0 | 1 | 0 | 1 | 5 | 1 | 1 | 1 |
| 09_大阪 | 459 | 316 | 262 | 139 | 151 | 100 | 25 | 104 | 184 | 122 | ... | 5 | 1 | 2 | 1 | 3 | 1 | 2 | 4 | 2 | 1 |
| 10_福岡 | 465 | 348 | 285 | 145 | 183 | 121 | 25 | 107 | 231 | 121 | ... | 1 | 2 | 0 | 0 | 1 | 3 | 2 | 7 | 8 | 0 |
12 rows × 1000 columns
1.2.3 Jaccard 係数を求める (外部変数-抽出語)¶
In [9]:
# 抽出語の出現回数を取得する
word_counts = cross_1000_df.sum(axis=0).values
# 属性(外部変数)出現数を取得する
attr_counts = np.hstack(
[
all_df.value_counts('カテゴリー').values,
all_df.value_counts('エリア').values
]
)
# 共起行列の中身を Jaccard 係数に入れ替える
jaccard_attrs_df = gssm_utils.jaccard_attrs_coef(aggregate_df, attr_counts, word_counts, total=10000, conditional=False)
# DataFrame を表示する
print(jaccard_attrs_df.shape)
display(jaccard_attrs_df)
(12, 1000)
| 表層 | 部屋 | 良い | ホテル | 風呂 | ない | 美味しい | 温泉 | スタッフ | 立地 | よい | ... | ほこり | 豊か | カラオケ | 支配人 | 頻繁 | 感覚 | 枕元 | コンサート | 店舗 | 人柄 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A_レジャー | 0.328157 | 0.318270 | 0.000000 | 0.265039 | 0.165314 | 0.268134 | 0.252298 | 0.166394 | 0.000000 | 0.121370 | ... | 0.000000 | 0.003197 | 0.003198 | 0.002598 | 0.002196 | 0.002396 | 0.000000 | 0.000000 | 0.000000 | 0.003398 |
| B_ビジネス | 0.000000 | 0.000000 | 0.236310 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.169791 | 0.000000 | ... | 0.002198 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.002997 | 0.003599 | 0.003799 | 0.000000 |
| 01_登別 | 0.000000 | 0.095540 | 0.000000 | 0.103714 | 0.075597 | 0.093931 | 0.127111 | 0.065581 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 02_草津 | 0.096052 | 0.107069 | 0.000000 | 0.135544 | 0.077708 | 0.112009 | 0.148662 | 0.078877 | 0.000000 | 0.063178 | ... | 0.000000 | 0.002950 | 0.003941 | 0.002962 | 0.007913 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.004926 |
| 03_箱根 | 0.108022 | 0.103535 | 0.000000 | 0.117418 | 0.078131 | 0.140490 | 0.107884 | 0.100955 | 0.000000 | 0.074321 | ... | 0.001974 | 0.004926 | 0.005923 | 0.002962 | 0.000000 | 0.003933 | 0.000000 | 0.000000 | 0.000000 | 0.003937 |
| 04_道後 | 0.000000 | 0.000000 | 0.077842 | 0.000000 | 0.000000 | 0.000000 | 0.106354 | 0.000000 | 0.079792 | 0.063679 | ... | 0.000000 | 0.000000 | 0.003941 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 05_湯布院 | 0.096914 | 0.092572 | 0.000000 | 0.107206 | 0.082808 | 0.139631 | 0.112500 | 0.109024 | 0.000000 | 0.066698 | ... | 0.000000 | 0.006910 | 0.001967 | 0.005941 | 0.002953 | 0.004921 | 0.000000 | 0.000000 | 0.000000 | 0.005917 |
| 06_札幌 | 0.090057 | 0.000000 | 0.096561 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.083080 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.002953 | 0.003933 | 0.000000 | 0.003941 | 0.002950 | 0.000000 |
| 07_名古屋 | 0.000000 | 0.000000 | 0.092388 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.070507 | 0.061176 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.003937 | 0.001967 | 0.004926 | 0.000000 |
| 08_東京 | 0.000000 | 0.000000 | 0.095800 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.074202 | 0.000000 | ... | 0.002964 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.004926 | 0.000000 | 0.000000 | 0.000000 |
| 09_大阪 | 0.089631 | 0.000000 | 0.090501 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.079792 | 0.000000 | ... | 0.004950 | 0.000000 | 0.001967 | 0.000000 | 0.002953 | 0.000000 | 0.000000 | 0.003941 | 0.000000 | 0.000000 |
| 10_福岡 | 0.090909 | 0.000000 | 0.099234 | 0.000000 | 0.071401 | 0.000000 | 0.000000 | 0.000000 | 0.102258 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.002947 | 0.000000 | 0.006917 | 0.007905 | 0.000000 |
12 rows × 1000 columns
1.2.4 共起ネットワーク図¶
In [10]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# サブルーチン
def sort_and_plot(name, group):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 共起行列の中身を Jaccard 係数に入れ替える
group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# プロットする
ax = fig.add_subplot(4, 3, i+1)
pyvis_plot = gssm_utils.plot_cooccur_network_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], pyvis=True, name=f"{name}.html")
ax.set_title(name)
display(pyvis_plot)
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
A_レジャー.html
01_登別.html
02_草津.html
03_箱根.html
04_道後.html
05_湯布院.html
B_ビジネス.html
06_札幌.html
07_名古屋.html
08_東京.html
09_大阪.html
10_福岡.html
1.2.5 係り受け行列を作成する (抽出語-抽出語)¶
In [11]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
# 共起行列を作成する
X = cross_1000_df.values
X = csc_matrix(X)
Xc = (X.T * X)
# 対角成分のみにする
Xc = np.triu(Xc.toarray())
# DataFrame 型に整える
cooccur_1000_df = pd.DataFrame(Xc, columns=cross_1000_df.columns, index=cross_1000_df.columns)
# チャンク(文節)から単語を取り出す
def get_words(tree, from_chunk, stopwords):
# チャンク(文節)の開始位置を取得する
beg = from_chunk.token_pos
# チャンクの開始位置を取得する
end = from_chunk.token_pos + from_chunk.token_size
# 抽出語情報リストの初期化
words = []
# チャンク(文節)ごとのループ
for i in range(beg, end):
# チャンク中の形態素を取り出す
token = tree.token(i)
# 解析結果を要素ごとにバラす
features = token.feature.split(',')
# 品詞1 を取り出す
pos1 = features[0]
# 品詞2 を取り出す
pos2 = features[1] if len(features) > 1 else ""
# 原形 を取り出す
base = features[6] if len(features) > 6 else None
# 原型がストップワードに含まれない単語のみ抽出する
if base not in stopwords:
# 「名詞-一般」
if (pos1 == "名詞" and pos2 == "一般"):
base = base if base is not None else node.surface
postag = "名詞"
key = (base, postag)
# 抽出語情報をリストに追加する
words.append(key)
# 「形容動詞」
elif (pos1 == "名詞" and pos2 == "形容動詞語幹"):
base = base if base is not None else node.surface
base = f"{base}だ"
postag = "形容動詞"
key = (base, postag)
# 抽出語情報をリストに追加する
words.append(key)
# 「形容詞」
elif pos1 == "形容詞":
base = base if base is not None else node.surface
postag = "形容詞"
key = (base, postag)
# 抽出語情報をリストに追加する
words.append(key)
# 「未知語」
elif pos1 == "未知語":
base = base if base is not None else node.surface
postag = "未知語"
key = (base, postag)
# 抽出語情報をリストに追加する
words.append(key)
# 抽出語情報をリストを返却する
return words
# 必要ライブラリのインポート
import CaboCha
# cabocha の初期化
cp = CaboCha.Parser("-r ../tools/usr/local/etc/cabocharc")
# 半角->全角変換マクロを定義する
ZEN = "".join(chr(0xff01 + i) for i in range(94))
HAN = "".join(chr(0x21 + i) for i in range(94))
HAN2ZEN = str.maketrans(HAN, ZEN)
# ストップワードを定義する
# stopwords = ['する', 'ある', 'ない', 'いう', 'もの', 'こと', 'よう', 'なる', 'ほう']
stopwords = ['*'] # 原形に 「'*'」 が出力された場合に除去するため
# 係り受けペア辞書の初期化
pair_counts = defaultdict(lambda: 0)
pairs = []
# データ1行ごとのループ
for index, row in all_df.iterrows():
# 半角->全角変換した後で, cabocha で解析する
tree = cp.parse(row["コメント"].translate(HAN2ZEN))
# 解析結果から空でないチャンク(文節)のリストを集める
chunks = {}
key = 0
for i in range(tree.size()):
tok = tree.token(i)
if tok.chunk:
chunks[key] = tok.chunk
key += 1
# 係り元と係り先の単語情報(原形と品詞)を集める
for from_chunk in chunks.values():
# 係り先がなければスキップ
if from_chunk.link < 0:
continue
# 係り先のチャンク(文節)を取得する
to_chunk = chunks[from_chunk.link]
# 係り元の単語情報(原形と品詞)を取得する
from_words = get_words(tree, from_chunk, stopwords)
# 係り先の単語情報(原形と品詞)を取得する
to_words = get_words(tree, to_chunk, stopwords)
# 係り受けペアと頻度を収集する
for f in from_words:
for t in to_words:
key = (f[0], t[0])
pair_counts[key] += 1
# 係り受け行列を初期化する (共起行列と同じ形)
Xd = np.zeros(cooccur_1000_df.shape)
# 係り受けペアを係り受け列に変換する
for (f,t), v in pair_counts.items():
columns = list(cooccur_1000_df.columns)
if f in columns and t in columns:
i = columns.index(f)
j = columns.index(t)
Xd[i,j] = v
# DataFrme 型に整える
dep_1000_df = pd.DataFrame(Xd, columns=cooccur_1000_df.columns, index=cooccur_1000_df.columns)
print(dep_1000_df.shape)
display(dep_1000_df.head())
(1000, 1000)
| 表層 | 部屋 | 良い | ホテル | 風呂 | ない | 美味しい | 温泉 | スタッフ | 立地 | よい | ... | ほこり | 豊か | カラオケ | 支配人 | 頻繁 | 感覚 | 枕元 | コンサート | 店舗 | 人柄 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 表層 | |||||||||||||||||||||
| 部屋 | 0.0 | 8.0 | 0.0 | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 3.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 良い | 3.0 | 2.0 | 24.0 | 0.0 | 3.0 | 2.0 | 1.0 | 0.0 | 1.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| ホテル | 0.0 | 7.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 風呂 | 0.0 | 12.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 4.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| ない | 1.0 | 3.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
5 rows × 1000 columns
1.2.6 係り受けネットワーク図¶
In [12]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# サブルーチン
def sort_and_plot(name, group, pos):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 係り受け行列(条件付き確率)を初期化する (共起行列と同じ形)
Xd = np.zeros(group_cooccur_df.shape)
# 係り受けペアを係り受け行列(条件付き確率)に変換する
for (f,t), v in pair_counts.items():
# 係り元と係り先の両方が列に含まれる
columns = list(group_cooccur_df.columns)
if f in columns and t in columns:
i = columns.index(f)
j = columns.index(t)
# 条件付き確率(係り受け頻度/係り先出現回数)を求める
Xd[i,j] = v / word_counts[i]
# 係り受け行列(条件付き確率)を DataFrame 型に整える
group_dependency_df = pd.DataFrame(Xd, columns=group_cross_df.columns, index=group_cross_df.columns)
# プロットする
ax = fig.add_subplot(4, 3, pos+1)
pyvis_plot = gssm_utils.plot_dependency_network_ax(ax, group_dependency_df, word_counts, np.sort(group_dependency_df.values.reshape(-1))[::-1][120], pyvis=False, name=f"{name}.html")
ax.set_title(name)
# display(pyvis_plot)
# 係り受けペアを条件付き確率で降順にソートし20個表示する
Xc = group_dependency_df.values
words = group_dependency_df.columns
flat_indices = np.argsort(Xc.ravel())[::-1][:20]
row_indices, col_indices = np.unravel_index(flat_indices, Xc.shape)
for idx in range(len(flat_indices)):
value = Xc[row_indices[idx], col_indices[idx]]
print(f"[{name}] {words[row_indices[idx]]} - {words[col_indices[idx]]}: {value:.04f}")
print()
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group, i)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group, i)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
[A_レジャー] 感じ - 良い: 0.0343 [A_レジャー] 景色 - 良い: 0.0335 [A_レジャー] すごい - 良い: 0.0244 [A_レジャー] 雰囲気 - 良い: 0.0233 [A_レジャー] ご飯 - 美味しい: 0.0181 [A_レジャー] 気持ち - 良い: 0.0162 [A_レジャー] 気持ち - よい: 0.0162 [A_レジャー] 味 - 良い: 0.0160 [A_レジャー] 味 - 美味しい: 0.0160 [A_レジャー] いい - 宿: 0.0157 [A_レジャー] 皆さん - 良い: 0.0147 [A_レジャー] 優しい - 宿: 0.0136 [A_レジャー] 浴場 - 良い: 0.0124 [A_レジャー] おいしい - よい: 0.0114 [A_レジャー] いい - 感じ: 0.0112 [A_レジャー] 景色 - 最高: 0.0112 [A_レジャー] 最高 - 宿: 0.0109 [A_レジャー] 方々 - 皆さん: 0.0102 [A_レジャー] 多い - 美味しい: 0.0081 [A_レジャー] お湯 - 良い: 0.0080 [01_登別] 気分 - 悪い: 0.2857 [01_登別] 感じ - 良い: 0.1757 [01_登別] 景色 - 良い: 0.1176 [01_登別] すごい - 良い: 0.1053 [01_登別] ご飯 - 美味しい: 0.0943 [01_登別] 味 - 良い: 0.0857 [01_登別] 味 - 美味しい: 0.0857 [01_登別] 眺め - 最高: 0.0800 [01_登別] 眺め - 良い: 0.0800 [01_登別] いい - 宿: 0.0722 [01_登別] 浴場 - 良い: 0.0600 [01_登別] 最高 - 宿: 0.0547 [01_登別] いい - 感じ: 0.0515 [01_登別] 風呂 - 良い: 0.0405 [01_登別] 内容 - 悪い: 0.0400 [01_登別] 内容 - 良い: 0.0400 [01_登別] お湯 - 良い: 0.0400 [01_登別] 思い出 - 良い: 0.0400 [01_登別] お湯 - 最高: 0.0400 [01_登別] 景色 - 最高: 0.0392 [02_草津] 感じ - 良い: 0.1413 [02_草津] 雰囲気 - 良い: 0.1176 [02_草津] すごい - 良い: 0.1053 [02_草津] いい - 宿: 0.0897 [02_草津] ご飯 - 美味しい: 0.0862 [02_草津] 味 - 美味しい: 0.0769 [02_草津] 味 - 良い: 0.0769 [02_草津] いい - 感じ: 0.0641 [02_草津] 浴場 - 良い: 0.0625 [02_草津] 皆さん - 良い: 0.0588 [02_草津] おいしい - よい: 0.0577 [02_草津] 最高 - 宿: 0.0473 [02_草津] 方々 - 皆さん: 0.0444 [02_草津] 露天風呂 - 最高: 0.0396 [02_草津] 多い - 美味しい: 0.0364 [02_草津] 種類 - 少ない: 0.0339 [02_草津] 風呂 - 良い: 0.0319 [02_草津] 広い - 良い: 0.0315 [02_草津] お湯 - 良い: 0.0312 [02_草津] お湯 - 最高: 0.0312 [03_箱根] 感じ - 良い: 0.1857 [03_箱根] 景色 - 良い: 0.1250 [03_箱根] 雰囲気 - 良い: 0.0833 [03_箱根] 気持ち - 良い: 0.0750 [03_箱根] 気持ち - よい: 0.0750 [03_箱根] いい - 宿: 0.0737 [03_箱根] 最高 - 宿: 0.0660 [03_箱根] 皆さん - 良い: 0.0652 [03_箱根] ご飯 - 美味しい: 0.0633 [03_箱根] デザート - 美味しい: 0.0571 [03_箱根] 味 - 良い: 0.0556 [03_箱根] 優しい - 宿: 0.0556 [03_箱根] 味 - 美味しい: 0.0556 [03_箱根] おいしい - よい: 0.0536 [03_箱根] いい - 感じ: 0.0526 [03_箱根] 浴場 - 良い: 0.0488 [03_箱根] アメニティ - 良い: 0.0429 [03_箱根] 湯 - よい: 0.0417 [03_箱根] 景色 - 最高: 0.0417 [03_箱根] お湯 - 最高: 0.0392 [04_道後] ごはん - おいしい: 0.1429 [04_道後] バスタオル - ほしい: 0.0833 [04_道後] おいしい - よい: 0.0667 [04_道後] 浴衣 - にくい: 0.0588 [04_道後] 湯 - よい: 0.0513 [04_道後] メニュー - 少ない: 0.0455 [04_道後] 品数 - 少ない: 0.0455 [04_道後] 立地 - よい: 0.0380 [04_道後] サウナ - よい: 0.0370 [04_道後] ジュース - よい: 0.0345 [04_道後] ジュース - おいしい: 0.0345 [04_道後] ビジネス - にくい: 0.0345 [04_道後] お湯 - よい: 0.0286 [04_道後] 少ない - よい: 0.0263 [04_道後] 無料 - よい: 0.0244 [04_道後] 場所 - にくい: 0.0192 [04_道後] よい - ホテル: 0.0148 [04_道後] よい - 小さい: 0.0074 [04_道後] よい - よい: 0.0074 [04_道後] ホテル - ほしい: 0.0044 [05_湯布院] 感じ - 良い: 0.1548 [05_湯布院] 景色 - 良い: 0.1277 [05_湯布院] すごい - 良い: 0.1176 [05_湯布院] 味 - 良い: 0.0769 [05_湯布院] 味 - 美味しい: 0.0769 [05_湯布院] 雰囲気 - 良い: 0.0755 [05_湯布院] ご飯 - 美味しい: 0.0735 [05_湯布院] いい - 宿: 0.0700 [05_湯布院] 皆さん - 良い: 0.0508 [05_湯布院] いい - 感じ: 0.0500 [05_湯布院] 気持ち - よい: 0.0462 [05_湯布院] 気持ち - 良い: 0.0462 [05_湯布院] おいしい - よい: 0.0455 [05_湯布院] 景色 - 最高: 0.0426 [05_湯布院] 方々 - 皆さん: 0.0426 [05_湯布院] 多い - 美味しい: 0.0426 [05_湯布院] お湯 - 最高: 0.0408 [05_湯布院] お湯 - 良い: 0.0408 [05_湯布院] 最高 - 宿: 0.0400 [05_湯布院] 優しい - 宿: 0.0400 [B_ビジネス] 繁華 - 近い: 0.0400 [B_ビジネス] 電車 - やすい: 0.0290 [B_ビジネス] 洗い場 - 狭い: 0.0135 [B_ビジネス] ツイン - 狭い: 0.0122 [B_ビジネス] 高い - ホテル: 0.0099 [B_ビジネス] やすい - ホテル: 0.0098 [B_ビジネス] エレベーター - 狭い: 0.0069 [B_ビジネス] 大きい - やすい: 0.0066 [B_ビジネス] 清潔 - やすい: 0.0065 [B_ビジネス] 新しい - 快適: 0.0057 [B_ビジネス] ルーム - 狭い: 0.0054 [B_ビジネス] ビジネス - にくい: 0.0053 [B_ビジネス] ビジネス - やすい: 0.0053 [B_ビジネス] 駅 - 近い: 0.0050 [B_ビジネス] コンビニ - 近い: 0.0044 [B_ビジネス] 荷物 - ありがたい: 0.0041 [B_ビジネス] トイレ - 別: 0.0033 [B_ビジネス] やすい - 立地: 0.0033 [B_ビジネス] やすい - ありがたい: 0.0033 [B_ビジネス] 清潔 - 価格: 0.0032 [06_札幌] 洗い場 - 寒い: 0.0556 [06_札幌] 次 - 連: 0.0556 [06_札幌] やすい - ホテル: 0.0556 [06_札幌] 寒い - ほしい: 0.0476 [06_札幌] 高い - ホテル: 0.0465 [06_札幌] パン - 欲しい: 0.0435 [06_札幌] 水 - 欲しい: 0.0357 [06_札幌] 駅 - 近い: 0.0354 [06_札幌] 欲しい - ホテル: 0.0333 [06_札幌] 清潔 - やすい: 0.0328 [06_札幌] コーヒー - ほしい: 0.0312 [06_札幌] ルーム - 広い: 0.0278 [06_札幌] ビジネス - やすい: 0.0278 [06_札幌] コンビニ - 近い: 0.0270 [06_札幌] 新しい - 快適: 0.0222 [06_札幌] 荷物 - ありがたい: 0.0213 [06_札幌] ベッド - 広い: 0.0196 [06_札幌] やすい - 立地: 0.0185 [06_札幌] やすい - ありがたい: 0.0185 [06_札幌] 清潔 - 価格: 0.0164 [07_名古屋] 繁華 - 近い: 0.0968 [07_名古屋] 安い - よい: 0.0732 [07_名古屋] 広め - 嬉しい: 0.0714 [07_名古屋] 寝心地 - よい: 0.0714 [07_名古屋] 洗い場 - 狭い: 0.0588 [07_名古屋] コーヒー - 嬉しい: 0.0588 [07_名古屋] タイプ - 嬉しい: 0.0556 [07_名古屋] 高い - ホテル: 0.0444 [07_名古屋] 立地 - よい: 0.0427 [07_名古屋] 近い - よい: 0.0417 [07_名古屋] やすい - ホテル: 0.0390 [07_名古屋] メニュー - 嬉しい: 0.0385 [07_名古屋] エレベーター - 狭い: 0.0357 [07_名古屋] 大きい - やすい: 0.0333 [07_名古屋] ルーム - 狭い: 0.0294 [07_名古屋] コーヒー - よい: 0.0294 [07_名古屋] 清潔 - やすい: 0.0290 [07_名古屋] 駅 - 近い: 0.0263 [07_名古屋] 新しい - 快適: 0.0263 [07_名古屋] やすい - よい: 0.0260 [08_東京] コーヒー - 嬉しい: 0.0741 [08_東京] 値段 - ホテル: 0.0455 [08_東京] 高い - ホテル: 0.0444 [08_東京] コーヒー - ほしい: 0.0370 [08_東京] エレベーター - 狭い: 0.0370 [08_東京] 早い - ほしい: 0.0357 [08_東京] ドリンク - 嬉しい: 0.0345 [08_東京] 女性 - ほしい: 0.0333 [08_東京] ルーム - 狭い: 0.0323 [08_東京] ビジネス - にくい: 0.0250 [08_東京] 値段 - 高い: 0.0227 [08_東京] 値段 - ビジネス: 0.0227 [08_東京] 駅 - 近い: 0.0190 [08_東京] 他 - ホテル: 0.0189 [08_東京] トイレ - 別: 0.0185 [08_東京] 嬉しい - アメニティ: 0.0169 [08_東京] コンビニ - 近い: 0.0165 [08_東京] 場所 - 快適: 0.0159 [08_東京] 場所 - にくい: 0.0159 [08_東京] 狭い - にくい: 0.0128 [09_大阪] 電車 - やすい: 0.1111 [09_大阪] 洗い場 - 狭い: 0.0625 [09_大阪] ツイン - 狭い: 0.0526 [09_大阪] パン - 欲しい: 0.0476 [09_大阪] 値段 - ホテル: 0.0455 [09_大阪] 次 - 連: 0.0455 [09_大阪] やすい - ホテル: 0.0448 [09_大阪] コーヒー - ほしい: 0.0357 [09_大阪] 欲しい - ホテル: 0.0333 [09_大阪] サウナ - 欲しい: 0.0294 [09_大阪] ビジネス - やすい: 0.0286 [09_大阪] 水 - 欲しい: 0.0278 [09_大阪] 大きい - やすい: 0.0278 [09_大阪] 新しい - 快適: 0.0256 [09_大阪] エレベーター - 狭い: 0.0250 [09_大阪] エレベーター - 欲しい: 0.0250 [09_大阪] ルーム - 狭い: 0.0244 [09_大阪] 駅 - 近い: 0.0242 [09_大阪] コンビニ - 近い: 0.0230 [09_大阪] 値段 - ビジネス: 0.0227 [10_福岡] いい - ホテル: 0.1358 [10_福岡] 電車 - やすい: 0.1111 [10_福岡] 洗い場 - 狭い: 0.0556 [10_福岡] 目的 - 近い: 0.0526 [10_福岡] やすい - ホテル: 0.0517 [10_福岡] ロビー - ない: 0.0400 [10_福岡] エレベーター - 狭い: 0.0370 [10_福岡] 圏内 - ない: 0.0370 [10_福岡] 距離 - 近い: 0.0345 [10_福岡] 印象 - ホテル: 0.0323 [10_福岡] 大きい - やすい: 0.0312 [10_福岡] 清潔 - やすい: 0.0286 [10_福岡] 駅 - 近い: 0.0261 [10_福岡] コンビニ - 近い: 0.0256 [10_福岡] 荷物 - ありがたい: 0.0244 [10_福岡] ルーム - 狭い: 0.0238 [10_福岡] やすい - ありがたい: 0.0172 [10_福岡] やすい - 立地: 0.0172 [10_福岡] ない - やすい: 0.0164 [10_福岡] 徒歩 - ない: 0.0147
1.3 高評価のエリアに倣って、低評価のエリアを改善するプランを提案する¶
1.3.1 対照的な2エリアを選択する¶
In [13]:
# コーディングルール
coding_pos = ["良い","美味しい","広い","多い","素晴らしい","嬉しい","気持ちよい","楽しい","近い","大きい","気持ち良い","温かい","早い","優しい","新しい","暖かい","快い","明るい","美しい","可愛い","満足"]
coding_neg = ["古い","無い","高い","悪い","小さい","狭い","少ない","寒い","遅い","熱い","欲しい","暑い","冷たい","遠い","臭い","暗い","うるさい","ない","無い","残念","改善","不満"]
In [14]:
# DataFrame を初期化する
cross_1000_ps_df = cross_1000_df.copy()
cross_1000_ps_df['ポジ'] = 0
cross_1000_ps_df['ネガ'] = 0
cross_1000_ps_df['総合1-2'] = 0
cross_1000_ps_df['総合4-5'] = 0
# コーディングルールを適用する (ポジ・ネガ)
pos_index = docs_df['表層'].str.contains("|".join(coding_pos))
neg_index = docs_df['表層'].str.contains("|".join(coding_neg))
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(docs_df.loc[pos_index, '文書ID']), 'ポジ'] = 1
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(docs_df.loc[neg_index, '文書ID']), 'ネガ'] = 1
# コーディングルールを適用する (総合評価)
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(all_df[all_df['総合'] <=2].index), '総合1-2'] = 1
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(all_df[all_df['総合'] >=4].index), '総合4-5'] = 1
cross_1000_ps_df = cross_1000_ps_df[['ポジ','ネガ','総合1-2','総合4-5']]
# DataFrame を表示する
print(cross_1000_ps_df.shape)
display(cross_1000_ps_df)
(9914, 4)
| 表層 | ポジ | ネガ | 総合1-2 | 総合4-5 | ||
|---|---|---|---|---|---|---|
| カテゴリー | エリア | 文書ID | ||||
| A_レジャー | 01_登別 | 1 | 1 | 0 | 0 | 1 |
| 2 | 1 | 1 | 0 | 1 | ||
| 3 | 0 | 0 | 0 | 1 | ||
| 4 | 1 | 0 | 1 | 0 | ||
| 5 | 1 | 1 | 0 | 1 | ||
| ... | ... | ... | ... | ... | ... | ... |
| B_ビジネス | 10_福岡 | 9996 | 1 | 0 | 0 | 1 |
| 9997 | 0 | 1 | 0 | 1 | ||
| 9998 | 0 | 0 | 0 | 1 | ||
| 9999 | 1 | 0 | 0 | 1 | ||
| 10000 | 1 | 0 | 0 | 0 |
9914 rows × 4 columns
In [15]:
# 「カテゴリー」のクロス集計と「エリア」のクロス集計を連結する
aggregate_ps_df = pd.concat(
[
cross_1000_ps_df.groupby(level='カテゴリー').sum(),
cross_1000_ps_df.groupby(level='エリア').sum()
]
)
# DataFrame を表示する
print(aggregate_ps_df.shape)
display(aggregate_ps_df)
(12, 4)
| 表層 | ポジ | ネガ | 総合1-2 | 総合4-5 |
|---|---|---|---|---|
| A_レジャー | 3798 | 2297 | 282 | 4279 |
| B_ビジネス | 3230 | 2105 | 289 | 4032 |
| 01_登別 | 729 | 472 | 69 | 822 |
| 02_草津 | 803 | 497 | 56 | 843 |
| 03_箱根 | 793 | 495 | 63 | 857 |
| 04_道後 | 668 | 405 | 47 | 854 |
| 05_湯布院 | 805 | 428 | 47 | 903 |
| 06_札幌 | 666 | 399 | 48 | 825 |
| 07_名古屋 | 642 | 435 | 40 | 828 |
| 08_東京 | 641 | 421 | 70 | 776 |
| 09_大阪 | 655 | 409 | 52 | 815 |
| 10_福岡 | 626 | 441 | 79 | 788 |
In [16]:
# 必要ライブラリのインポート
import mca
# ライブラリ mca による対応分析
ncols = aggregate_ps_df.shape[1]
mca_ben = mca.MCA(aggregate_ps_df, ncols=ncols, benzecri=False)
# 行方向および列方向の値を取り出す
row_coord = mca_ben.fs_r(N=2)
col_coord = mca_ben.fs_c(N=2)
# 固有値を求める
eigenvalues = mca_ben.L
total = np.sum(eigenvalues)
# 寄与率を求める
explained_inertia = 100 * eigenvalues / total
# 行方向および列方向のラベルを取得する
row_labels = aggregate_ps_df.index
col_labels = aggregate_ps_df.columns
# プロットする
gssm_utils.plot_coresp(row_coord, col_coord, row_labels, col_labels, explained_inertia)
1.3.2 ポジティブ意見の共起ネットワーク図を作成する¶
In [17]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# コーディングルール
coding_or = coding_pos
# サブルーチン
def sort_and_plot(name, group):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# コーディングルールで絞り込む
index = docs_df['表層'].str.contains("|".join(coding_or))
group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 共起行列の中身を Jaccard 係数に入れ替える
group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# プロットする
ax = fig.add_subplot(4, 3, i+1)
pyvis_plot = gssm_utils.plot_cooccur_network_with_code_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], coding_or, pyvis=True, name=f"pos-{name}.html")
ax.set_title(name)
display(pyvis_plot)
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
pos-A_レジャー.html
pos-01_登別.html
pos-02_草津.html
pos-03_箱根.html
pos-04_道後.html
pos-05_湯布院.html
pos-B_ビジネス.html
pos-06_札幌.html
pos-07_名古屋.html
pos-08_東京.html
pos-09_大阪.html
pos-10_福岡.html
1.3.3 ポジティブ意見の係り受けネットワーク図を作成する¶
In [18]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# コーディングルール
coding_or = coding_pos
# サブルーチン
def sort_and_plot(name, group, pos):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# コーディングルールで絞り込む
index = docs_df['表層'].str.contains("|".join(coding_or))
group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 係り受け行列(条件付き確率)を初期化する (共起行列と同じ形)
Xd = np.zeros(group_cooccur_df.shape)
# 係り受けペアを係り受け行列(条件付き確率)に変換する
for (f,t), v in pair_counts.items():
# 係り元と係り先の両方が列に含まれる
columns = list(group_cooccur_df.columns)
if f in columns and t in columns:
i = columns.index(f)
j = columns.index(t)
# 条件付き確率(係り受け頻度/係り先出現回数)を求める
Xd[i,j] = v / word_counts[i]
# 係り受け行列(条件付き確率)を DataFrame 型に整える
group_dependency_df = pd.DataFrame(Xd, columns=group_cross_df.columns, index=group_cross_df.columns)
# プロットする
ax = fig.add_subplot(4, 3, pos+1)
pyvis_plot = gssm_utils.plot_cooccur_network_with_code_ax(ax, group_dependency_df, word_counts, np.sort(group_dependency_df.values.reshape(-1))[::-1][120], coding_or, pyvis=False, name=f"pos-{name}.html")
ax.set_title(name)
# display(pyvis_plot)
# 係り受けペアを条件付き確率で降順にソートし20個表示する
Xc = group_dependency_df.values
words = group_dependency_df.columns
flat_indices = np.argsort(Xc.ravel())[::-1][:20]
row_indices, col_indices = np.unravel_index(flat_indices, Xc.shape)
for idx in range(len(flat_indices)):
value = Xc[row_indices[idx], col_indices[idx]]
print(f"[{name}] {words[row_indices[idx]]} - {words[col_indices[idx]]}: {value:.04f}")
print()
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group, i)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group, i)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
[A_レジャー] 感じ - 良い: 0.0343 [A_レジャー] 景色 - 良い: 0.0335 [A_レジャー] すごい - 良い: 0.0244 [A_レジャー] 雰囲気 - 良い: 0.0233 [A_レジャー] ご飯 - 美味しい: 0.0181 [A_レジャー] 気持ち - 良い: 0.0162 [A_レジャー] 気持ち - よい: 0.0162 [A_レジャー] 味 - 良い: 0.0160 [A_レジャー] 味 - 美味しい: 0.0160 [A_レジャー] いい - 宿: 0.0157 [A_レジャー] 皆さん - 良い: 0.0147 [A_レジャー] 優しい - 宿: 0.0136 [A_レジャー] 浴場 - 良い: 0.0124 [A_レジャー] おいしい - よい: 0.0114 [A_レジャー] いい - 感じ: 0.0112 [A_レジャー] 景色 - 最高: 0.0112 [A_レジャー] 最高 - 宿: 0.0109 [A_レジャー] 方々 - 皆さん: 0.0102 [A_レジャー] 多い - 美味しい: 0.0081 [A_レジャー] お湯 - 良い: 0.0080 [01_登別] 気分 - 悪い: 0.2857 [01_登別] 感じ - 良い: 0.1757 [01_登別] 景色 - 良い: 0.1176 [01_登別] すごい - 良い: 0.1053 [01_登別] ご飯 - 美味しい: 0.0943 [01_登別] 味 - 良い: 0.0857 [01_登別] 味 - 美味しい: 0.0857 [01_登別] 眺め - 最高: 0.0800 [01_登別] 眺め - 良い: 0.0800 [01_登別] いい - 宿: 0.0722 [01_登別] 浴場 - 良い: 0.0600 [01_登別] 最高 - 宿: 0.0547 [01_登別] いい - 感じ: 0.0515 [01_登別] 風呂 - 良い: 0.0405 [01_登別] 内容 - 悪い: 0.0400 [01_登別] 内容 - 良い: 0.0400 [01_登別] お湯 - 良い: 0.0400 [01_登別] 思い出 - 良い: 0.0400 [01_登別] お湯 - 最高: 0.0400 [01_登別] 景色 - 最高: 0.0392 [02_草津] 感じ - 良い: 0.1413 [02_草津] 雰囲気 - 良い: 0.1176 [02_草津] すごい - 良い: 0.1053 [02_草津] いい - 宿: 0.0897 [02_草津] ご飯 - 美味しい: 0.0862 [02_草津] 味 - 美味しい: 0.0769 [02_草津] 味 - 良い: 0.0769 [02_草津] いい - 感じ: 0.0641 [02_草津] 浴場 - 良い: 0.0625 [02_草津] 皆さん - 良い: 0.0588 [02_草津] おいしい - よい: 0.0577 [02_草津] 最高 - 宿: 0.0473 [02_草津] 方々 - 皆さん: 0.0444 [02_草津] 露天風呂 - 最高: 0.0396 [02_草津] 多い - 美味しい: 0.0364 [02_草津] 種類 - 少ない: 0.0339 [02_草津] 風呂 - 良い: 0.0319 [02_草津] 広い - 良い: 0.0315 [02_草津] お湯 - 良い: 0.0312 [02_草津] お湯 - 最高: 0.0312 [03_箱根] 感じ - 良い: 0.1857 [03_箱根] 景色 - 良い: 0.1250 [03_箱根] 雰囲気 - 良い: 0.0833 [03_箱根] 気持ち - 良い: 0.0750 [03_箱根] 気持ち - よい: 0.0750 [03_箱根] いい - 宿: 0.0737 [03_箱根] 最高 - 宿: 0.0660 [03_箱根] 皆さん - 良い: 0.0652 [03_箱根] ご飯 - 美味しい: 0.0633 [03_箱根] デザート - 美味しい: 0.0571 [03_箱根] 味 - 良い: 0.0556 [03_箱根] 優しい - 宿: 0.0556 [03_箱根] 味 - 美味しい: 0.0556 [03_箱根] おいしい - よい: 0.0536 [03_箱根] いい - 感じ: 0.0526 [03_箱根] 浴場 - 良い: 0.0488 [03_箱根] アメニティ - 良い: 0.0429 [03_箱根] 湯 - よい: 0.0417 [03_箱根] 景色 - 最高: 0.0417 [03_箱根] お湯 - 最高: 0.0392 [04_道後] ごはん - おいしい: 0.1429 [04_道後] バスタオル - ほしい: 0.0833 [04_道後] おいしい - よい: 0.0667 [04_道後] 浴衣 - にくい: 0.0588 [04_道後] 湯 - よい: 0.0513 [04_道後] メニュー - 少ない: 0.0455 [04_道後] 品数 - 少ない: 0.0455 [04_道後] 立地 - よい: 0.0380 [04_道後] サウナ - よい: 0.0370 [04_道後] ジュース - よい: 0.0345 [04_道後] ジュース - おいしい: 0.0345 [04_道後] ビジネス - にくい: 0.0345 [04_道後] お湯 - よい: 0.0286 [04_道後] 少ない - よい: 0.0263 [04_道後] 無料 - よい: 0.0244 [04_道後] 場所 - にくい: 0.0192 [04_道後] よい - ホテル: 0.0148 [04_道後] よい - 小さい: 0.0074 [04_道後] よい - よい: 0.0074 [04_道後] ホテル - ほしい: 0.0044 [05_湯布院] 感じ - 良い: 0.1548 [05_湯布院] 景色 - 良い: 0.1277 [05_湯布院] すごい - 良い: 0.1176 [05_湯布院] 味 - 良い: 0.0769 [05_湯布院] 味 - 美味しい: 0.0769 [05_湯布院] 雰囲気 - 良い: 0.0755 [05_湯布院] ご飯 - 美味しい: 0.0735 [05_湯布院] いい - 宿: 0.0700 [05_湯布院] 皆さん - 良い: 0.0508 [05_湯布院] いい - 感じ: 0.0500 [05_湯布院] 気持ち - よい: 0.0462 [05_湯布院] 気持ち - 良い: 0.0462 [05_湯布院] おいしい - よい: 0.0455 [05_湯布院] 景色 - 最高: 0.0426 [05_湯布院] 方々 - 皆さん: 0.0426 [05_湯布院] 多い - 美味しい: 0.0426 [05_湯布院] お湯 - 最高: 0.0408 [05_湯布院] お湯 - 良い: 0.0408 [05_湯布院] 最高 - 宿: 0.0400 [05_湯布院] 優しい - 宿: 0.0400 [B_ビジネス] 繁華 - 近い: 0.0400 [B_ビジネス] 電車 - やすい: 0.0290 [B_ビジネス] 洗い場 - 狭い: 0.0135 [B_ビジネス] ツイン - 狭い: 0.0122 [B_ビジネス] 高い - ホテル: 0.0099 [B_ビジネス] やすい - ホテル: 0.0098 [B_ビジネス] エレベーター - 狭い: 0.0069 [B_ビジネス] 大きい - やすい: 0.0066 [B_ビジネス] 清潔 - やすい: 0.0065 [B_ビジネス] 新しい - 快適: 0.0057 [B_ビジネス] ルーム - 狭い: 0.0054 [B_ビジネス] ビジネス - にくい: 0.0053 [B_ビジネス] ビジネス - やすい: 0.0053 [B_ビジネス] 駅 - 近い: 0.0050 [B_ビジネス] コンビニ - 近い: 0.0044 [B_ビジネス] 荷物 - ありがたい: 0.0041 [B_ビジネス] トイレ - 別: 0.0033 [B_ビジネス] やすい - 立地: 0.0033 [B_ビジネス] やすい - ありがたい: 0.0033 [B_ビジネス] 清潔 - 価格: 0.0032 [06_札幌] 洗い場 - 寒い: 0.0556 [06_札幌] 次 - 連: 0.0556 [06_札幌] やすい - ホテル: 0.0556 [06_札幌] 寒い - ほしい: 0.0476 [06_札幌] 高い - ホテル: 0.0465 [06_札幌] パン - 欲しい: 0.0435 [06_札幌] 水 - 欲しい: 0.0357 [06_札幌] 駅 - 近い: 0.0354 [06_札幌] 欲しい - ホテル: 0.0333 [06_札幌] 清潔 - やすい: 0.0328 [06_札幌] コーヒー - ほしい: 0.0312 [06_札幌] ルーム - 広い: 0.0278 [06_札幌] ビジネス - やすい: 0.0278 [06_札幌] コンビニ - 近い: 0.0270 [06_札幌] 新しい - 快適: 0.0222 [06_札幌] 荷物 - ありがたい: 0.0213 [06_札幌] ベッド - 広い: 0.0196 [06_札幌] やすい - 立地: 0.0185 [06_札幌] やすい - ありがたい: 0.0185 [06_札幌] 清潔 - 価格: 0.0164 [07_名古屋] 繁華 - 近い: 0.0968 [07_名古屋] 安い - よい: 0.0732 [07_名古屋] 広め - 嬉しい: 0.0714 [07_名古屋] 寝心地 - よい: 0.0714 [07_名古屋] 洗い場 - 狭い: 0.0588 [07_名古屋] コーヒー - 嬉しい: 0.0588 [07_名古屋] タイプ - 嬉しい: 0.0556 [07_名古屋] 高い - ホテル: 0.0444 [07_名古屋] 立地 - よい: 0.0427 [07_名古屋] 近い - よい: 0.0417 [07_名古屋] やすい - ホテル: 0.0390 [07_名古屋] メニュー - 嬉しい: 0.0385 [07_名古屋] エレベーター - 狭い: 0.0357 [07_名古屋] 大きい - やすい: 0.0333 [07_名古屋] ルーム - 狭い: 0.0294 [07_名古屋] コーヒー - よい: 0.0294 [07_名古屋] 清潔 - やすい: 0.0290 [07_名古屋] 駅 - 近い: 0.0263 [07_名古屋] 新しい - 快適: 0.0263 [07_名古屋] やすい - よい: 0.0260 [08_東京] コーヒー - 嬉しい: 0.0741 [08_東京] 値段 - ホテル: 0.0455 [08_東京] 高い - ホテル: 0.0444 [08_東京] コーヒー - ほしい: 0.0370 [08_東京] エレベーター - 狭い: 0.0370 [08_東京] 早い - ほしい: 0.0357 [08_東京] ドリンク - 嬉しい: 0.0345 [08_東京] 女性 - ほしい: 0.0333 [08_東京] ルーム - 狭い: 0.0323 [08_東京] ビジネス - にくい: 0.0250 [08_東京] 値段 - 高い: 0.0227 [08_東京] 値段 - ビジネス: 0.0227 [08_東京] 駅 - 近い: 0.0190 [08_東京] 他 - ホテル: 0.0189 [08_東京] トイレ - 別: 0.0185 [08_東京] 嬉しい - アメニティ: 0.0169 [08_東京] コンビニ - 近い: 0.0165 [08_東京] 場所 - 快適: 0.0159 [08_東京] 場所 - にくい: 0.0159 [08_東京] 狭い - にくい: 0.0128 [09_大阪] 電車 - やすい: 0.1111 [09_大阪] 洗い場 - 狭い: 0.0625 [09_大阪] ツイン - 狭い: 0.0526 [09_大阪] パン - 欲しい: 0.0476 [09_大阪] 値段 - ホテル: 0.0455 [09_大阪] 次 - 連: 0.0455 [09_大阪] やすい - ホテル: 0.0448 [09_大阪] コーヒー - ほしい: 0.0357 [09_大阪] 欲しい - ホテル: 0.0333 [09_大阪] サウナ - 欲しい: 0.0294 [09_大阪] ビジネス - やすい: 0.0286 [09_大阪] 水 - 欲しい: 0.0278 [09_大阪] 大きい - やすい: 0.0278 [09_大阪] 新しい - 快適: 0.0256 [09_大阪] エレベーター - 狭い: 0.0250 [09_大阪] エレベーター - 欲しい: 0.0250 [09_大阪] ルーム - 狭い: 0.0244 [09_大阪] 駅 - 近い: 0.0242 [09_大阪] コンビニ - 近い: 0.0230 [09_大阪] 値段 - ビジネス: 0.0227 [10_福岡] いい - ホテル: 0.1358 [10_福岡] 電車 - やすい: 0.1111 [10_福岡] 洗い場 - 狭い: 0.0556 [10_福岡] 目的 - 近い: 0.0526 [10_福岡] やすい - ホテル: 0.0517 [10_福岡] ロビー - ない: 0.0400 [10_福岡] エレベーター - 狭い: 0.0370 [10_福岡] 圏内 - ない: 0.0370 [10_福岡] 距離 - 近い: 0.0345 [10_福岡] 印象 - ホテル: 0.0323 [10_福岡] 大きい - やすい: 0.0312 [10_福岡] 清潔 - やすい: 0.0286 [10_福岡] 駅 - 近い: 0.0261 [10_福岡] コンビニ - 近い: 0.0256 [10_福岡] 荷物 - ありがたい: 0.0244 [10_福岡] ルーム - 狭い: 0.0238 [10_福岡] やすい - ありがたい: 0.0172 [10_福岡] やすい - 立地: 0.0172 [10_福岡] ない - やすい: 0.0164 [10_福岡] 徒歩 - ない: 0.0147
1.3.4 ネガティブ意見の共起ネットワーク図を作成する¶
In [19]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# コーディングルール
coding_or = coding_neg
# サブルーチン
def sort_and_plot(name, group):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# コーディングルールで絞り込む
index = docs_df['表層'].str.contains("|".join(coding_or))
group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 共起行列の中身を Jaccard 係数に入れ替える
group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# プロットする
ax = fig.add_subplot(4, 3, i+1)
pyvis_plot = gssm_utils.plot_cooccur_network_with_code_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], coding_or, pyvis=True, name=f"neg-{name}.html")
ax.set_title(name)
display(pyvis_plot)
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
neg-A_レジャー.html
neg-01_登別.html
neg-02_草津.html
neg-03_箱根.html
neg-04_道後.html
neg-05_湯布院.html
neg-B_ビジネス.html
neg-06_札幌.html
neg-07_名古屋.html
neg-08_東京.html
neg-09_大阪.html
neg-10_福岡.html
1.3.5 ネガティブ意見係り受けネットワーク図を作成する¶
In [20]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline
# コーディングルール
coding_or = coding_neg
# サブルーチン
def sort_and_plot(name, group, pos):
# 「カテゴリー」ごとに Jaccard 係数でソートする
sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
# Jaccard 係数Top 75語をソートして抽出する
group_cross_df = group.iloc[:,sorted_columns]
# 抽出語の出現回数を取得する
word_counts = group_cross_df.sum(axis=0).values
# 共起行列を作成する
X = group_cross_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())
# コーディングルールで絞り込む
index = docs_df['表層'].str.contains("|".join(coding_or))
group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]
# 共起行列を DataFrame に整える
group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
# 係り受け行列(条件付き確率)を初期化する (共起行列と同じ形)
Xd = np.zeros(group_cooccur_df.shape)
# 係り受けペアを係り受け行列(条件付き確率)に変換する
for (f,t), v in pair_counts.items():
# 係り元と係り先の両方が列に含まれる
columns = list(group_cooccur_df.columns)
if f in columns and t in columns:
i = columns.index(f)
j = columns.index(t)
# 条件付き確率(係り受け頻度/係り先出現回数)を求める
Xd[i,j] = v / word_counts[i]
# 係り受け行列(条件付き確率)を DataFrame 型に整える
group_dependency_df = pd.DataFrame(Xd, columns=group_cross_df.columns, index=group_cross_df.columns)
# プロットする
ax = fig.add_subplot(4, 3, pos+1)
pyvis_plot = gssm_utils.plot_cooccur_network_with_code_ax(ax, group_dependency_df, word_counts, np.sort(group_dependency_df.values.reshape(-1))[::-1][120], coding_or, pyvis=False, name=f"pos-{name}.html")
ax.set_title(name)
# display(pyvis_plot)
# 係り受けペアを条件付き確率で降順にソートし20個表示する
Xc = group_dependency_df.values
words = group_dependency_df.columns
flat_indices = np.argsort(Xc.ravel())[::-1][:20]
row_indices, col_indices = np.unravel_index(flat_indices, Xc.shape)
for idx in range(len(flat_indices)):
value = Xc[row_indices[idx], col_indices[idx]]
print(f"[{name}] {words[row_indices[idx]]} - {words[col_indices[idx]]}: {value:.04f}")
print()
# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))
i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
# サブルーチンを呼ぶ
sort_and_plot(name, group, i)
i += 1
# エリアごとのループ
for sub_name, sub_group in group.groupby(level='エリア'):
# サブルーチンを呼ぶ
sort_and_plot(sub_name, sub_group, i)
i += 1
# プロットの仕上げ
plt.tight_layout()
plt.show()
[A_レジャー] 感じ - 良い: 0.0343 [A_レジャー] 景色 - 良い: 0.0335 [A_レジャー] すごい - 良い: 0.0244 [A_レジャー] 雰囲気 - 良い: 0.0233 [A_レジャー] ご飯 - 美味しい: 0.0181 [A_レジャー] 気持ち - 良い: 0.0162 [A_レジャー] 気持ち - よい: 0.0162 [A_レジャー] 味 - 良い: 0.0160 [A_レジャー] 味 - 美味しい: 0.0160 [A_レジャー] いい - 宿: 0.0157 [A_レジャー] 皆さん - 良い: 0.0147 [A_レジャー] 優しい - 宿: 0.0136 [A_レジャー] 浴場 - 良い: 0.0124 [A_レジャー] おいしい - よい: 0.0114 [A_レジャー] いい - 感じ: 0.0112 [A_レジャー] 景色 - 最高: 0.0112 [A_レジャー] 最高 - 宿: 0.0109 [A_レジャー] 方々 - 皆さん: 0.0102 [A_レジャー] 多い - 美味しい: 0.0081 [A_レジャー] お湯 - 良い: 0.0080 [01_登別] 気分 - 悪い: 0.2857 [01_登別] 感じ - 良い: 0.1757 [01_登別] 景色 - 良い: 0.1176 [01_登別] すごい - 良い: 0.1053 [01_登別] ご飯 - 美味しい: 0.0943 [01_登別] 味 - 良い: 0.0857 [01_登別] 味 - 美味しい: 0.0857 [01_登別] 眺め - 最高: 0.0800 [01_登別] 眺め - 良い: 0.0800 [01_登別] いい - 宿: 0.0722 [01_登別] 浴場 - 良い: 0.0600 [01_登別] 最高 - 宿: 0.0547 [01_登別] いい - 感じ: 0.0515 [01_登別] 風呂 - 良い: 0.0405 [01_登別] 内容 - 悪い: 0.0400 [01_登別] 内容 - 良い: 0.0400 [01_登別] お湯 - 良い: 0.0400 [01_登別] 思い出 - 良い: 0.0400 [01_登別] お湯 - 最高: 0.0400 [01_登別] 景色 - 最高: 0.0392 [02_草津] 感じ - 良い: 0.1413 [02_草津] 雰囲気 - 良い: 0.1176 [02_草津] すごい - 良い: 0.1053 [02_草津] いい - 宿: 0.0897 [02_草津] ご飯 - 美味しい: 0.0862 [02_草津] 味 - 美味しい: 0.0769 [02_草津] 味 - 良い: 0.0769 [02_草津] いい - 感じ: 0.0641 [02_草津] 浴場 - 良い: 0.0625 [02_草津] 皆さん - 良い: 0.0588 [02_草津] おいしい - よい: 0.0577 [02_草津] 最高 - 宿: 0.0473 [02_草津] 方々 - 皆さん: 0.0444 [02_草津] 露天風呂 - 最高: 0.0396 [02_草津] 多い - 美味しい: 0.0364 [02_草津] 種類 - 少ない: 0.0339 [02_草津] 風呂 - 良い: 0.0319 [02_草津] 広い - 良い: 0.0315 [02_草津] お湯 - 良い: 0.0312 [02_草津] お湯 - 最高: 0.0312 [03_箱根] 感じ - 良い: 0.1857 [03_箱根] 景色 - 良い: 0.1250 [03_箱根] 雰囲気 - 良い: 0.0833 [03_箱根] 気持ち - 良い: 0.0750 [03_箱根] 気持ち - よい: 0.0750 [03_箱根] いい - 宿: 0.0737 [03_箱根] 最高 - 宿: 0.0660 [03_箱根] 皆さん - 良い: 0.0652 [03_箱根] ご飯 - 美味しい: 0.0633 [03_箱根] デザート - 美味しい: 0.0571 [03_箱根] 味 - 良い: 0.0556 [03_箱根] 優しい - 宿: 0.0556 [03_箱根] 味 - 美味しい: 0.0556 [03_箱根] おいしい - よい: 0.0536 [03_箱根] いい - 感じ: 0.0526 [03_箱根] 浴場 - 良い: 0.0488 [03_箱根] アメニティ - 良い: 0.0429 [03_箱根] 湯 - よい: 0.0417 [03_箱根] 景色 - 最高: 0.0417 [03_箱根] お湯 - 最高: 0.0392 [04_道後] ごはん - おいしい: 0.1429 [04_道後] バスタオル - ほしい: 0.0833 [04_道後] おいしい - よい: 0.0667 [04_道後] 浴衣 - にくい: 0.0588 [04_道後] 湯 - よい: 0.0513 [04_道後] メニュー - 少ない: 0.0455 [04_道後] 品数 - 少ない: 0.0455 [04_道後] 立地 - よい: 0.0380 [04_道後] サウナ - よい: 0.0370 [04_道後] ジュース - よい: 0.0345 [04_道後] ジュース - おいしい: 0.0345 [04_道後] ビジネス - にくい: 0.0345 [04_道後] お湯 - よい: 0.0286 [04_道後] 少ない - よい: 0.0263 [04_道後] 無料 - よい: 0.0244 [04_道後] 場所 - にくい: 0.0192 [04_道後] よい - ホテル: 0.0148 [04_道後] よい - 小さい: 0.0074 [04_道後] よい - よい: 0.0074 [04_道後] ホテル - ほしい: 0.0044 [05_湯布院] 感じ - 良い: 0.1548 [05_湯布院] 景色 - 良い: 0.1277 [05_湯布院] すごい - 良い: 0.1176 [05_湯布院] 味 - 良い: 0.0769 [05_湯布院] 味 - 美味しい: 0.0769 [05_湯布院] 雰囲気 - 良い: 0.0755 [05_湯布院] ご飯 - 美味しい: 0.0735 [05_湯布院] いい - 宿: 0.0700 [05_湯布院] 皆さん - 良い: 0.0508 [05_湯布院] いい - 感じ: 0.0500 [05_湯布院] 気持ち - よい: 0.0462 [05_湯布院] 気持ち - 良い: 0.0462 [05_湯布院] おいしい - よい: 0.0455 [05_湯布院] 景色 - 最高: 0.0426 [05_湯布院] 方々 - 皆さん: 0.0426 [05_湯布院] 多い - 美味しい: 0.0426 [05_湯布院] お湯 - 最高: 0.0408 [05_湯布院] お湯 - 良い: 0.0408 [05_湯布院] 最高 - 宿: 0.0400 [05_湯布院] 優しい - 宿: 0.0400 [B_ビジネス] 繁華 - 近い: 0.0400 [B_ビジネス] 電車 - やすい: 0.0290 [B_ビジネス] 洗い場 - 狭い: 0.0135 [B_ビジネス] ツイン - 狭い: 0.0122 [B_ビジネス] 高い - ホテル: 0.0099 [B_ビジネス] やすい - ホテル: 0.0098 [B_ビジネス] エレベーター - 狭い: 0.0069 [B_ビジネス] 大きい - やすい: 0.0066 [B_ビジネス] 清潔 - やすい: 0.0065 [B_ビジネス] 新しい - 快適: 0.0057 [B_ビジネス] ルーム - 狭い: 0.0054 [B_ビジネス] ビジネス - にくい: 0.0053 [B_ビジネス] ビジネス - やすい: 0.0053 [B_ビジネス] 駅 - 近い: 0.0050 [B_ビジネス] コンビニ - 近い: 0.0044 [B_ビジネス] 荷物 - ありがたい: 0.0041 [B_ビジネス] トイレ - 別: 0.0033 [B_ビジネス] やすい - 立地: 0.0033 [B_ビジネス] やすい - ありがたい: 0.0033 [B_ビジネス] 清潔 - 価格: 0.0032 [06_札幌] 洗い場 - 寒い: 0.0556 [06_札幌] 次 - 連: 0.0556 [06_札幌] やすい - ホテル: 0.0556 [06_札幌] 寒い - ほしい: 0.0476 [06_札幌] 高い - ホテル: 0.0465 [06_札幌] パン - 欲しい: 0.0435 [06_札幌] 水 - 欲しい: 0.0357 [06_札幌] 駅 - 近い: 0.0354 [06_札幌] 欲しい - ホテル: 0.0333 [06_札幌] 清潔 - やすい: 0.0328 [06_札幌] コーヒー - ほしい: 0.0312 [06_札幌] ルーム - 広い: 0.0278 [06_札幌] ビジネス - やすい: 0.0278 [06_札幌] コンビニ - 近い: 0.0270 [06_札幌] 新しい - 快適: 0.0222 [06_札幌] 荷物 - ありがたい: 0.0213 [06_札幌] ベッド - 広い: 0.0196 [06_札幌] やすい - 立地: 0.0185 [06_札幌] やすい - ありがたい: 0.0185 [06_札幌] 清潔 - 価格: 0.0164 [07_名古屋] 繁華 - 近い: 0.0968 [07_名古屋] 安い - よい: 0.0732 [07_名古屋] 広め - 嬉しい: 0.0714 [07_名古屋] 寝心地 - よい: 0.0714 [07_名古屋] 洗い場 - 狭い: 0.0588 [07_名古屋] コーヒー - 嬉しい: 0.0588 [07_名古屋] タイプ - 嬉しい: 0.0556 [07_名古屋] 高い - ホテル: 0.0444 [07_名古屋] 立地 - よい: 0.0427 [07_名古屋] 近い - よい: 0.0417 [07_名古屋] やすい - ホテル: 0.0390 [07_名古屋] メニュー - 嬉しい: 0.0385 [07_名古屋] エレベーター - 狭い: 0.0357 [07_名古屋] 大きい - やすい: 0.0333 [07_名古屋] ルーム - 狭い: 0.0294 [07_名古屋] コーヒー - よい: 0.0294 [07_名古屋] 清潔 - やすい: 0.0290 [07_名古屋] 駅 - 近い: 0.0263 [07_名古屋] 新しい - 快適: 0.0263 [07_名古屋] やすい - よい: 0.0260 [08_東京] コーヒー - 嬉しい: 0.0741 [08_東京] 値段 - ホテル: 0.0455 [08_東京] 高い - ホテル: 0.0444 [08_東京] コーヒー - ほしい: 0.0370 [08_東京] エレベーター - 狭い: 0.0370 [08_東京] 早い - ほしい: 0.0357 [08_東京] ドリンク - 嬉しい: 0.0345 [08_東京] 女性 - ほしい: 0.0333 [08_東京] ルーム - 狭い: 0.0323 [08_東京] ビジネス - にくい: 0.0250 [08_東京] 値段 - 高い: 0.0227 [08_東京] 値段 - ビジネス: 0.0227 [08_東京] 駅 - 近い: 0.0190 [08_東京] 他 - ホテル: 0.0189 [08_東京] トイレ - 別: 0.0185 [08_東京] 嬉しい - アメニティ: 0.0169 [08_東京] コンビニ - 近い: 0.0165 [08_東京] 場所 - 快適: 0.0159 [08_東京] 場所 - にくい: 0.0159 [08_東京] 狭い - にくい: 0.0128 [09_大阪] 電車 - やすい: 0.1111 [09_大阪] 洗い場 - 狭い: 0.0625 [09_大阪] ツイン - 狭い: 0.0526 [09_大阪] パン - 欲しい: 0.0476 [09_大阪] 値段 - ホテル: 0.0455 [09_大阪] 次 - 連: 0.0455 [09_大阪] やすい - ホテル: 0.0448 [09_大阪] コーヒー - ほしい: 0.0357 [09_大阪] 欲しい - ホテル: 0.0333 [09_大阪] サウナ - 欲しい: 0.0294 [09_大阪] ビジネス - やすい: 0.0286 [09_大阪] 水 - 欲しい: 0.0278 [09_大阪] 大きい - やすい: 0.0278 [09_大阪] 新しい - 快適: 0.0256 [09_大阪] エレベーター - 狭い: 0.0250 [09_大阪] エレベーター - 欲しい: 0.0250 [09_大阪] ルーム - 狭い: 0.0244 [09_大阪] 駅 - 近い: 0.0242 [09_大阪] コンビニ - 近い: 0.0230 [09_大阪] 値段 - ビジネス: 0.0227 [10_福岡] いい - ホテル: 0.1358 [10_福岡] 電車 - やすい: 0.1111 [10_福岡] 洗い場 - 狭い: 0.0556 [10_福岡] 目的 - 近い: 0.0526 [10_福岡] やすい - ホテル: 0.0517 [10_福岡] ロビー - ない: 0.0400 [10_福岡] エレベーター - 狭い: 0.0370 [10_福岡] 圏内 - ない: 0.0370 [10_福岡] 距離 - 近い: 0.0345 [10_福岡] 印象 - ホテル: 0.0323 [10_福岡] 大きい - やすい: 0.0312 [10_福岡] 清潔 - やすい: 0.0286 [10_福岡] 駅 - 近い: 0.0261 [10_福岡] コンビニ - 近い: 0.0256 [10_福岡] 荷物 - ありがたい: 0.0244 [10_福岡] ルーム - 狭い: 0.0238 [10_福岡] やすい - ありがたい: 0.0172 [10_福岡] やすい - 立地: 0.0172 [10_福岡] ない - やすい: 0.0164 [10_福岡] 徒歩 - ない: 0.0147
1.3.4 本文の参照 (エリアで絞る)¶
「登別」と「道後」で「すばらしい」という単語が含まれている口コミを表示する
In [21]:
# 検索条件
search_index = \
all_df['エリア'].isin(['01_登別', '05_道後']) & \
(all_df['コメント'].str.contains('素晴らしい') | all_df['コメント'].str.contains('すばらしい'))
# 検索する
result_df = all_df[search_index]
# DataFrame を表示する
print(result_df.shape)
display(result_df.head())
# CSV に保存する
result_df.to_csv("output-1.csv", header=True)
(41, 18)
| カテゴリー | エリア | 施設番号 | 施設名 | コメント | 総合 | サービス | 立地 | 部屋 | 設備・アメニティ | 風呂 | 食事 | 旅行の目的 | 同伴者 | 宿泊年月 | 投稿者 | 年代 | 性別 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 68 | A_レジャー | 01_登別 | 51198 | 望楼NOGUCHI登別 | ホテルスタッフの対応素晴らしいですね!夕飯の和とフレンチのコース料理美味しかったですよー | 4 | 3 | 4 | 3 | 3.0 | 5.0 | 5.0 | レジャー | 恋人 | 45413 | 投稿者 | na | na |
| 114 | A_レジャー | 01_登別 | 172346 | ザ・レイクスイート湖の栖(グランベルホテルズ&リゾーツ) | 部屋のベッドの固さ、枕は最適でした。どこのメーカーか調べるの忘れました。アメニティ他用意され... | 5 | 5 | 5 | 5 | 5.0 | 5.0 | 3.0 | レジャー | 家族 | 45170 | 投稿者 | na | na |
| 148 | A_レジャー | 01_登別 | 12568 | 登別温泉 ホテル まほろば | 何度宿泊しても素晴らしいホテルです。初めてGREEN Terrace で食事しましたが…リバ... | 5 | 5 | 5 | 5 | 5.0 | 5.0 | 5.0 | レジャー | 恋人 | 45108 | T.Y0922 | 50代 | 女性 |
| 162 | A_レジャー | 01_登別 | 40708 | 虎杖浜温泉 ホテル いずみ | 2度目の利用でした。とにかく素晴らしい泉質の温泉や風呂からの眺めが楽しめる宿です。施設や部屋... | 5 | 5 | 5 | 3 | 3.0 | 5.0 | 3.0 | レジャー | 一人 | 45323 | 投稿者 | na | na |
| 198 | A_レジャー | 01_登別 | 164980 | 森の湯 山静館 | 口コミ通りの素晴らしい温泉です。登別温泉街とは源泉が異なり、硫黄臭が無くトロトロです。浴室に... | 5 | 5 | 4 | 4 | 3.0 | 5.0 | 5.0 | レジャー | 家族 | 45231 | とみい社長 | 60代 | 男性 |
「東京」と「福岡」で「うるさい」という単語が含まれている口コミを表示する
In [22]:
# 検索条件
search_index = \
all_df['エリア'].isin(['08_東京', '10_福岡']) & \
all_df['コメント'].str.contains('うるさい')
# 検索する
result_df = all_df[search_index]
# DataFrame を表示する
print(result_df.shape)
display(result_df.head())
# CSV に保存する
result_df.to_csv("output-2.csv", header=True)
(9, 18)
| カテゴリー | エリア | 施設番号 | 施設名 | コメント | 総合 | サービス | 立地 | 部屋 | 設備・アメニティ | 風呂 | 食事 | 旅行の目的 | 同伴者 | 宿泊年月 | 投稿者 | 年代 | 性別 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7064 | B_ビジネス | 08_東京 | 164596 | IMANO TOKYO GINZA HOSTEL | 部屋は10階の6人部屋女性専用フロアです。17:30にチェックインし、荷物を置いて友達と遊び... | 4 | 4 | 5 | 4 | 4.0 | 3.0 | NaN | その他 | 一人 | 44927 | 投稿者 | na | na |
| 7215 | B_ビジネス | 08_東京 | 2024 | レンブラントスタイル東京西葛西 | 部屋がとにかく暗すぎです。Wi-Fiが途切れ最悪でした。空気清浄機がうるさい騒音で全く効果な... | 2 | 3 | 5 | 1 | 1.0 | 1.0 | NaN | レジャー | 家族 | 44958 | 投稿者 | na | na |
| 7217 | B_ビジネス | 08_東京 | 2447 | ダイヤモンドホテル | エアコンディショナーがうるさいです。コストからして、立地以外には利点がない。 | 2 | 3 | 4 | 2 | 2.0 | 3.0 | 3.0 | ビジネス | 一人 | 45323 | 投稿者 | na | na |
| 7327 | B_ビジネス | 08_東京 | 179995 | unito CHIYODA | 場所、入口が分かりにくい。夜中にアジア系外人の電話うるさい。部屋の通路狭すぎコスパは1980... | 3 | 2 | 3 | 2 | 3.0 | 3.0 | NaN | ビジネス | 一人 | 45292 | 木村次郎応援団 | 60代 | 男性 |
| 7492 | B_ビジネス | 08_東京 | 18248 | ビジネスホテル福千 | 駅近でJRとメトロがありアクセス良好です。線路が近いので電車の音はうるさいです。耳栓の持参を... | 4 | 3 | 5 | 3 | 3.0 | 3.0 | NaN | レジャー | 一人 | 45292 | はたけ9495 | 40代 | 男性 |